# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Controlled unitary gate."""

from typing import List, Optional, Union
from qiskit.circuit.exceptions import CircuitError

# pylint: disable=cyclic-import
from .quantumcircuit import QuantumCircuit
from .gate import Gate
from .quantumregister import QuantumRegister

# pylint: disable=missing-return-doc


class ControlledGate(Gate):
    """Controlled unitary gate."""

    def __init__(self, name: str, num_qubits: int, params: List,
                 label: Optional[str] = None, num_ctrl_qubits: Optional[int] = 1,
                 definition: Optional['QuantumCircuit'] = None,
                 ctrl_state: Optional[Union[int, str]] = None):
        """Create a new ControlledGate. In the new gate the first ``num_ctrl_qubits``
        of the gate are the controls.

        Args:
            name: The name of the gate.
            num_qubits: The number of qubits the gate acts on.
            params: A list of parameters for the gate.
            label: An optional label for the gate.
            num_ctrl_qubits: Number of control qubits.
            definition: A list of gate rules for implementing this gate. The
                elements of the list are tuples of (:meth:`~qiskit.circuit.Gate`, [qubit_list],
                [clbit_list]).
            ctrl_state: The control state in decimal or as
                a bitstring (e.g. '111'). If specified as a bitstring the length
                must equal num_ctrl_qubits, MSB on left. If None, use
                2**num_ctrl_qubits-1.

        Raises:
            CircuitError: If ``num_ctrl_qubits`` >= ``num_qubits``.
            CircuitError: ctrl_state < 0 or ctrl_state > 2**num_ctrl_qubits.

        Examples:

        Create a controlled standard gate and apply it to a circuit.

        .. jupyter-execute::

           from qiskit import QuantumCircuit, QuantumRegister
           from qiskit.circuit.library.standard_gates import HGate

           qr = QuantumRegister(3)
           qc = QuantumCircuit(qr)
           c3h_gate = HGate().control(2)
           qc.append(c3h_gate, qr)
           qc.draw()

        Create a controlled custom gate and apply it to a circuit.

        .. jupyter-execute::

           from qiskit import QuantumCircuit, QuantumRegister
           from qiskit.circuit.library.standard_gates import HGate

           qc1 = QuantumCircuit(2)
           qc1.x(0)
           qc1.h(1)
           custom = qc1.to_gate().control(2)

           qc2 = QuantumCircuit(4)
           qc2.append(custom, [0, 3, 1, 2])
           qc2.draw()
        """
        super().__init__(name, num_qubits, params, label=label)
        self._num_ctrl_qubits = 1
        self.num_ctrl_qubits = num_ctrl_qubits
        self.base_gate = None
        if definition:
            self.definition = definition
            if len(definition) == 1:
                base_gate = definition.data[0][0]
                if isinstance(base_gate, ControlledGate):
                    self.base_gate = base_gate.base_gate
                else:
                    self.base_gate = base_gate
        self._ctrl_state = None
        self.ctrl_state = ctrl_state

    @property
    def definition(self) -> List:
        """Return definition in terms of other basic gates. If the gate has
        open controls, as determined from `self.ctrl_state`, the returned
        definition is conjugated with X without changing the internal
        `_definition`.
        """
        if self._open_ctrl:
            closed_gate = self.copy()
            closed_gate.ctrl_state = None
            bit_ctrl_state = bin(self.ctrl_state)[2:].zfill(self.num_ctrl_qubits)
            qreg = QuantumRegister(self.num_qubits, 'q')
            qc_open_ctrl = QuantumCircuit(qreg)
            for qind, val in enumerate(bit_ctrl_state[::-1]):
                if val == '0':
                    qc_open_ctrl.x(qind)
            qc_open_ctrl.append(closed_gate, qargs=qreg[:])
            for qind, val in enumerate(bit_ctrl_state[::-1]):
                if val == '0':
                    qc_open_ctrl.x(qind)
            return qc_open_ctrl
        else:
            return super().definition

    @definition.setter
    def definition(self, excited_def: 'QuantumCircuit'):
        """Set controlled gate definition with closed controls.

        Args:
            excited_def: The circuit with all closed controls."""
        super(Gate, self.__class__).definition.fset(self, excited_def)

    @property
    def num_ctrl_qubits(self):
        """Get number of control qubits.

        Returns:
            int: The number of control qubits for the gate.
        """
        return self._num_ctrl_qubits

    @num_ctrl_qubits.setter
    def num_ctrl_qubits(self, num_ctrl_qubits):
        """Set the number of control qubits.

        Args:
            num_ctrl_qubits (int): The number of control qubits in [1, num_qubits-1].

        Raises:
            CircuitError: num_ctrl_qubits is not an integer in [1, num_qubits - 1].
        """
        if (num_ctrl_qubits == int(num_ctrl_qubits) and
                1 <= num_ctrl_qubits < self.num_qubits):
            self._num_ctrl_qubits = num_ctrl_qubits
        else:
            raise CircuitError('The number of control qubits must be in [1, num_qubits-1]')

    @property
    def ctrl_state(self) -> int:
        """Return the control state of the gate as a decimal integer."""
        return self._ctrl_state

    @ctrl_state.setter
    def ctrl_state(self, ctrl_state: Union[int, str, None]):
        """Set the control state of this gate.

        Args:
            ctrl_state: The control state of the gate.

        Raises:
            CircuitError: ctrl_state is invalid.
        """
        if isinstance(ctrl_state, str):
            try:
                assert len(ctrl_state) == self.num_ctrl_qubits
                ctrl_state = int(ctrl_state, 2)
            except ValueError:
                raise CircuitError('invalid control bit string: ' + ctrl_state)
            except AssertionError:
                raise CircuitError('invalid control bit string: length != '
                                   'num_ctrl_qubits')
        if isinstance(ctrl_state, int):
            if 0 <= ctrl_state < 2**self.num_ctrl_qubits:
                self._ctrl_state = ctrl_state
            else:
                raise CircuitError('invalid control state specification')
        elif ctrl_state is None:
            self._ctrl_state = 2**self.num_ctrl_qubits - 1
        else:
            raise CircuitError('invalid control state specification: {}'.format(
                repr(ctrl_state)))

    @property
    def _open_ctrl(self) -> bool:
        """Return whether gate has any open controls"""
        return self.ctrl_state < 2**self.num_ctrl_qubits - 1

    def __eq__(self, other) -> bool:
        return (isinstance(other, ControlledGate) and
                self.num_ctrl_qubits == other.num_ctrl_qubits and
                self.ctrl_state == other.ctrl_state and
                self.base_gate == other.base_gate and
                self.num_qubits == other.num_qubits and
                self.num_clbits == other.num_clbits and
                self.definition == other.definition)

    def inverse(self) -> 'ControlledGate':
        """Invert this gate by calling inverse on the base gate."""
        return self.base_gate.inverse().control(self.num_ctrl_qubits,
                                                ctrl_state=self.ctrl_state)
